%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Типичная раскладка памяти процесса: код, статическое хранилище, куча и стек"
%%| fig-width: 5.5
%%| fig-height: 4
flowchart TB
Code["Сегмент кода<br/>скомпилированные инструкции"]
Static["Статическое / глобальное хранилище<br/>глобальные и static-объекты"]
Heap["Куча (heap)<br/>динамика через new"]
Stack["Стек (stack)<br/>локальные переменные и кадры вызовов"]
Code --> Static --> Heap --> Stack
W1. Введение в C++, пространства имён, массивы и векторы, ссылки и константы, вывод типов
1. Краткое содержание
1.1 Организация курса
Курс Software Systems Analysis and Design (анализ и проектирование программных систем) построен из нескольких компонентов: теория и практика программирования на C++ сочетаются так, чтобы вы получили и концептуальную базу, и навыки написания кода.
1.1.1 Компоненты курса
Курс включает три основных компонента:
- Лекции — теоретические концепции и основы языка: programming concepts и возможности C++ на концептуальном уровне.
- Туториалы — дополнительные примеры и подробные разборы тем лекций: отдельные аспекты и практическое применение теории.
- Лабораторные работы — практика программирования: упражнения и задания, чтобы закрепить навыки.
1.1.2 Оценивание
Итоговая оценка складывается из нескольких частей:
- Mid-term Exam (промежуточный экзамен): 25% (викторина в Moodle, 10 марта)
- Final Exam (итоговый экзамен): 30% (письменная форма)
- Assignments (домашние задания / ассайнменты): 40% (4 задания, регулярная проверка)
- Посещаемость лаб: 5%
- Бонус: 5%
Шкала: A [90, 100], B [75, 90), C [60, 75), D [0, 60).
1.2 Как работают программы на C++
1.2.1 От исходного кода до исполняемого файла
Понимание цепочки выполнения проясняет многие дальнейшие темы. Путь кода выглядит так:
- Source code (исходный код): вы пишете C++ в файлах
.cpp(текст для человека). - Preprocessing (препроцессинг): препроцессор обрабатывает директивы вроде
#include(подключение заголовков) и#define(макросы). - Compilation (компиляция): compiler (компилятор) переводит C++ в machine code (машинный код) — двоичные инструкции для CPU.
- Linking (линковка): linker (линкер) объединяет скомпилированный код с библиотеками (например C++ Standard Library) в executable (исполняемый файл).
- Execution (исполнение): ОС загружает и запускает исполняемый файл.
Ключевое различие:
- Compile time (время компиляции): шаги 2–4 до запуска программы. Компилятор проверяет типы, вычисляет constant expressions и генерирует машинный код.
- Runtime (время выполнения): шаг 5 — когда программа реально работает: ввод пользователя, dynamic memory allocation и логика программы.
1.2.2 Что такое компилятор?
Compiler (компилятор) — программа, которая переводит source code в machine code. Распространённые компиляторы C++:
- GCC (GNU Compiler Collection) — свободный, широко используемый
- Clang — современный, с понятными сообщениями об ошибках
- MSVC (Microsoft Visual C++) — для разработки под Windows
Задачи компилятора включают:
- проверку syntax (синтаксиса);
- type checking (проверку типов);
- optimization (оптимизацию);
- code generation (генерацию кода).
1.2.3 Стандарты C++
C++ развивается; в разных версиях появляются новые возможности:
- C++98: первый стандартизованный вариант
- C++11: крупное обновление (auto, range-based for, nullptr)
- C++14: небольшие улучшения
- C++17: structured bindings, optional, variant
- C++20: concepts, ranges, coroutines, modules
- C++23 и C++26: ещё больше возможностей
В этом курсе используем стандарт C++20: доступны все средства вплоть до C++20 включительно.
1.3 Основы программирования на C++
1.3.1 Первая программа на C++
Базовая программа состоит из нескольких обязательных элементов:
#include <iostream>
int main()
{
std::cout << "Hello world" << std::endl;
return 0;
}Разберём каждый фрагмент:
#include <iostream>— preprocessor directive (директива препроцессора): подключает стандартную библиотеку ввода-вывода и даёт сущности вродеcoutдля вывода в консоль.int main()— main function (функцияmain), с которой начинается выполнение. В каждой программе на C++ ровно однаmain; среду исполнения интересует именно она.std::cout— standard output stream (стандартный поток вывода) для печати в консоль; это часть пространства имёнstd.<<— stream insertion operator (оператор вставки в поток), перегруженный для потоков ввода-вывода (для целых это был бы сдвиг, здесь — другая семантика).std::endl— символ конца строки и flush буфера вывода.return 0;завершаетmainи возвращает 0 окружению как признак успешного завершения.
1.3.2 Модель памяти
В программе на C++ различают три вида памяти с разными ролями:
- Program memory (память кода): скомпилированные инструкции; обычно только для чтения — самомодифицирующийся код в C++ не допускается.
- Heap (куча, dynamic memory): объекты, выделенные в runtime через
newи подобные средства; программист задаёт моменты выделения/освобождения. Дисциплина кучи задаётся dynamic semantics (динамической семантикой). - Stack (стек): локальные переменные и информация о вызовах функций; управляется автоматически по области видимости и стеку вызовов. Дисциплина стека следует из static program structure (статической структуры программы), определяемой на этапе компиляции.
1.4 Система типов
1.4.1 Что такое тип?
Type (тип) — базовое понятие C++: для сущности тип задаёт три аспекта:
- Values (множество значений), которые может хранить объект.
- Operations (операции), допустимые над объектами этого типа.
- Relationships (связи): преобразования к другим типам, inheritance и т.д.
Например, тип int:
- Values: целые в типичном диапазоне (часто от −2 147 483 648 до 2 147 483 647).
- Operations: создание, уничтожение, копирование, перемещение, арифметика (+, −, *, /), сравнения (==, <, >), побитовые операции и сдвиги.
- Relationships: преобразования к
bool,float,doubleи др.
1.4.2 Иерархия типов C++
Типы C++ удобно мыслить иерархически.
Fundamental types (фундаментальные типы):
- Atomic types (атомарные типы): целые (
int,short,long,long long), символы (char,wchar_t,char16_t,char32_t,char8_t), вещественные (float,double,long double), логический (bool). - Pointers (указатели): переменные-адреса.
User-defined types (пользовательские типы):
- Compound types (составные типы): массивы, структуры, объединения, классы, перечисления.
1.4.3 Syntax vs semantics
Важное различие в языках программирования:
- Syntax (синтаксис): правила структуры конструкций («грамматика» языка).
- Semantics (семантика) — смысл конструкций:
- Static semantics (статическая семантика): как программа компилируется и проверяется по типам.
- Dynamic semantics (динамическая семантика): как программа выполняется в runtime.
Важный принцип: синтаксис относительно лёгок, а семантика C++ огромна и сложна. Опирайтесь на понимание того, что делает код, а не только на правила записи.
1.5 Пространства имён (namespaces)
1.5.1 Зачем это нужно
В больших проектах разные части кода могут использовать одни и те же имена в разных смыслах — возникают name clashes (конфликты имён). C++ даёт namespaces (пространства имён) как способ сгруппировать объявления и развести имена.
Namespace — декларативная область, задающая область видимости для идентификаторов (типов, функций, переменных и т.д.) внутри неё; так глобальная область дробится на именованные части.
1.5.2 Синтаксис namespace
Пространство имён задаётся ключевым словом namespace:
namespace Subsystem1
{
class C1 { ... };
int a, b;
void f() { ... }
}
namespace Subsystem2
{
class C2 { ... };
int a; // This is a DIFFERENT 'a' from Subsystem1::a
}1.5.3 Доступ к членам пространства имён
Снаружи namespace сущности доступны через qualified naming (квалифицированные имена) и scope resolution operator :::
Формат: namespace-name::entity-name
int x = Subsystem1::a;
Subsystem1::f();1.5.4 Глобальное пространство имён
Вся программа живёт в unnamed (global) namespace (безымянном глобальном пространстве имён). Чтобы явно обратиться к глобальной области, используйте :: без имени namespace:
int a = 5; // Global variable
namespace Subsystem
{
int a = 10; // Different variable
}
int x = Subsystem::a; // x = 10
int y = a; // y = 5 (global a)
int z = ::a; // z = 5 (explicitly global a)1.5.5 Пространство имён стандартной библиотеки
Сущности C++ Standard Library объявлены в std. Типичные примеры:
std::cout,std::cin(I/O streams)std::vector,std::list,std::map(контейнеры)std::string(строковый класс)std::endl(манипулятор конца строки)
1.5.6 Using-объявления
Чтобы упростить запись, using declarations вводят имена из namespace в текущую область:
using namespace std; // Brings ALL std members into scope
cout << "Hello" << endl; // No need for std:: prefixОднако для крупных программ using namespace std; считается плохой практикой: теряется смысл namespaces. Лучше импортировать выборочно:
using std::cout;
using std::endl;
cout << "Hello" << endl; // Only cout and endl are imported1.5.7 Продвинутые возможности namespace
Multi-file namespaces: одно namespace может охватывать несколько translation units (исходных файлов):
// File 1
namespace Subsystem1 {
class C1 { ... };
}
// File 2
namespace Subsystem1 {
class C2 { ... }; // Still part of Subsystem1
}Вложенные namespace: иерархическая организация:
namespace OurBigSystem
{
namespace MySubsystem
{
int a;
}
}
int x = OurBigSystem::MySubsystem::a;1.5.8 Зачем нужны пространства имён
Представьте приложение с двумя библиотеками: в Library A и Library B есть print() с разным смыслом. Без namespaces имена конфликтуют — компилятор не поймёт, какой print() вызывать.
С namespaces:
LibraryA::print(document); // Prints a document
LibraryB::print("Debug info"); // Prints debug messageПоэтому стандартная библиотека использует префикс std:: — чтобы не пересекаться с вашим кодом.
1.6 Указатели (pointers)
1.6.1 Что такое указатель?
Перед массивами нужны pointers (указатели): на них завязаны память и массивы в C++.
Pointer — переменная, хранящая memory address (адрес памяти). Вместо значения вроде 5 или 3.14 в ней — место, где значение лежит.
Аналогия: память как многоквартирный дом; у каждой «квартиры» (memory location):
- address (адрес);
- contents (содержимое ячейки).
Обычная переменная даёт прямой доступ к содержимому; pointer — к адресу, по которому можно снова получить содержимое.
1.6.2 Синтаксис указателей
Объявление указателя:
int* ptr; // ptr is a pointer to an integer
double* dptr; // dptr is a pointer to a double
char* cptr; // cptr is a pointer to a characterВ объявлении * читается как «указатель на». Запись int* ptr — «ptr — pointer to int».
1.6.3 Операции с указателями
Две ключевые операции:
Address-of operator (&) (оператор взятия адреса): даёт адрес переменной.
int x = 42;
int* ptr = &x; // ptr now holds the address of x&x можно читать как «адрес, где живёт x».
Dereference operator (*) (оператор разыменования): доступ к значению по адресу в pointer.
int x = 42;
int* ptr = &x; // ptr points to x
int value = *ptr; // value = 42 (get the value at the address)
*ptr = 100; // Changes x to 100 (modify the value at the address)*ptr — «перейти по адресу в ptr и взять то, что там лежит».
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Базовые указатели: указатель хранит адрес, разыменование ведёт к значению по этому адресу"
%%| fig-width: 6
%%| fig-height: 2.8
flowchart LR
P["p : int*<br/>значение = &a"]
A["a : int<br/>значение = 5"]
P -- "хранит адрес" --> A
D["*p == 5"]
A --> D
1.6.4 Указатели на практике
Полный пример:
int a = 5;
int* p = &a; // p points to a
cout << a; // Prints: 5 (direct access)
cout << p; // Prints: address of a (e.g., 0x7fff5fbff5ac)
cout << *p; // Prints: 5 (indirect access through pointer)
*p = 10; // Changes a to 10
cout << a; // Prints: 10Ключевая идея: меняя *p, вы меняете a, потому что в p хранится address (адрес) a.
1.6.5 Зачем нужны указатели
Pointers нужны, в частности, для:
- Dynamic memory allocation (динамическое выделение памяти): объекты, размер или время жизни которых неизвестны на этапе компиляции
- Efficient function parameters (эффективные параметры функций): передача крупных объектов по адресу вместо копирования
- Data structures (структуры данных): связные списки, деревья, графы и т.д.
- Low-level programming (низкоуровневое программирование): работа с оборудованием и API ОС
1.6.6 Null pointers
Указатель можно установить в nullptr (в старом коде часто NULL), чтобы явно обозначить, что он ни на что валидное не ссылается:
int* ptr = nullptr; // ptr doesn't point anywhere
if (ptr == nullptr) {
cout << "Pointer is null" << endl;
}
// *ptr = 5; // DANGEROUS! Would crash the programВсегда инициализируйте указатели либо валидным адресом, либо nullptr. Неинициализированный указатель содержит «мусор» и часто приводит к трудноуловимым ошибкам.
1.7 Массивы и векторы (arrays vs. vectors)
1.7.1 Массивы в стиле C
Разобравшись с pointers, проще понять arrays (массивы): это низкоуровневый механизм, унаследованный от C. Объявление массива задаёт фиксированный набор элементов одного типа.
Синтаксис: T arrayName[size];
T— тип элементовsize— constant expression (константное выражение), известное на этапе компиляции
int Array[10]; // Array of 10 integers
const int x = 7;
void* Ptrs[x*2+5]; // Array of 19 void pointers
int Matrix[10][100]; // 2D arrayОсновные свойства:
- Fixed size (фиксированный размер): после объявления не меняется
- No bounds checking (нет проверки границ): выход за пределы — undefined behavior
- Decay to pointers (преобразование имени массива в указатель): имя массива ведёт себя как константный указатель на первый элемент
1.7.2 Массивы и указатели
Здесь сходятся arrays и pointers: имя массива по сути — constant pointer (константный указатель) на первый элемент. Это центральная идея для C++.
int Array[10];
// Array is equivalent to: const int* ArrayДоступ к элементам:
Array[0]эквивалентно*ArrayArray[i]эквивалентно*(Array + i)(pointer arithmetic)
Зачем это важно: понимая array decay, вы объясняете себе типичное поведение массивов:
- при передаче массива в функцию на самом деле передаётся указатель;
- массив «не знает» собственный размер — его нужно вести отдельно;
- контроль границ — ваша ответственность.
Тесная связь массивов и указателей мощна, но и источник многих багов — поэтому в C++ есть более удобная альтернатива: vectors.
1.7.3 Векторы C++
Vectors (std::vector) — часть C++ Standard Library; это более безопасная и гибкая замена сырому массиву. По сути это dynamically-sized arrays с богатым набором операций.
Отличия от массивов:
| Свойство | Массивы | Векторы |
|---|---|---|
| Объявление | int A[20]; |
vector<int> A; |
| Размер | Фиксируется при компиляции | Меняется в runtime |
| Проверка границ | Нет | Есть через .at() |
| Возможности | В основном индексация | Много методов |
| Производительность | Максимальная | Почти как у массивов |
| Безопасность | Низкая | Значительно выше |
1.7.4 Операции с вектором
Объявление и инициализация:
vector<int> v1; // Empty vector
vector<int> v2 = { 1, 2, 3 }; // Initialized with values
vector<int> v3(10); // 10 elements, default-initialized
vector<int> v4(10, 7); // 10 elements, all with value 7Добавление элементов:
vector<int> v;
v.push_back(10); // Add 10 to the end
v.push_back(20); // Add 20 to the end
v.push_back(30); // Now v = {10, 20, 30}Доступ к элементам:
vector<int> v = { 1, 2, 3 };
int x = v[0]; // Access first element (no bounds checking)
v[2] = 777; // Modify third element
int y = v.at(1); // Access with bounds checking (throws exception if out of range)Размер:
cout << v.size() << endl; // Returns number of elementsПолезные операции: v.clear(), v.empty(), v.front(), v.back(), v.erase(), v.insert() и др.
1.7.5 Range-based for
В C++11 появился удобный синтаксис обхода контейнеров (в том числе vectors):
Только чтение (элемент копируется):
vector<int> v = { 1, 2, 3, 4 };
for (int elem : v) {
cout << elem << " "; // elem is a copy of each element
}Изменение элементов (нужна ссылка):
for (int& elem : v) {
elem = elem * 10; // Modifies actual vector elements
}Вывод типа через auto:
for (auto& elem : v) {
elem = elem * 10; // Compiler deduces type automatically
}Range-based for работает и с массивами в стиле C, и со initializer lists:
int arr[] = {1, 2, 3};
for (int n : arr) {
cout << n << " ";
}
for (int n : {0, 1, 2, 3}) { // Initializer list
cout << n << " ";
}1.8 Ссылки (references)
1.8.1 Что такое reference?
После pointers проще понять references (ссылки). Reference — это alias (псевдоним) уже существующего объекта: другое имя для той же переменной.
После инициализации ссылка всегда относится к одному и тому же объекту; операции над ссылкой — это операции над объектом, на который она ссылается.
Синтаксис: T& refName = object;
int x = 5;
int& r = x; // r is now a reference to x
r = 7; // Same as: x = 7
x = 777;
int v = r; // v = 777 (reading x through r)Ключевая идея: r — не копия x, а то же самое x под другим именем; одна и та же область памяти.
1.8.2 References и pointers
И то и другое даёт indirection, но различия важны:
| Свойство | Указатели | Ссылки |
|---|---|---|
| Объявление | int* p; |
int& r = x; |
| Природа | Полноценные объекты | Не объекты, только псевдонимы |
| Значение | Хранят адрес | Нет «собственного» значения |
| Инициализация | Могут быть nullptr или мусором |
Обязательны при объявлении; null references нет |
| Переназначение | Можно направить на другой объект | Всегда тот же объект |
| Операторы | Явные & и * |
Дополнительные операторы не нужны |
| Синтаксис | *p = 5; |
r = 5; |
1.8.3 Зачем нужны references?
Главная мотивация — efficiency (эффективность): передача крупного объекта by value дорога. Reference даёт функции доступ к оригиналу без копирования.
void f(Huge a) { ... } // Expensive: copies entire Huge object
void f(Huge& a) { ... } // Efficient: passes reference (just an address)Наглядно: передать по значению — как сфотокопировать книгу целиком; передать по ссылке — как дать саму книгу.
1.8.4 Правила и ограничения
Так как ссылка не является отдельным объектом:
- нет указателей на ссылку;
- нет массивов ссылок;
- нет ссылки на ссылку (в смысле «вложенности типа» как у указателей);
- ссылку нужно инициализировать при объявлении.
При этом допустимо:
- ссылка на указатель:
int* p; int*& rp = p; - указатель на «ссылку как тип» в C++ не строится, но ссылка на указатель — да
1.9 Константные типы (const)
1.9.1 Квалификатор const
const задаёт constant types (константные типы): после инициализации объект нельзя менять.
const T описывает множество неизменяемых объектов типа T. Важно: T и const T — разные типы, хотя множество допустимых значений совпадает.
const int b = 777; // b cannot be modified
b = 5; // ERROR: cannot modify const variableЗачем const?
- Safety (безопасность): меньше случайных изменений
- Intent (намерение): явно показывает, что значение зафиксировано
- Optimization (оптимизация): компилятору проще упрощать код
1.9.2 Константы времени компиляции и выполнения
Compile-time constants инициализируются constant expressions (значение известно при компиляции):
const int b = 777; // Constant expression
const int c = 2 * b + 5; // Also constant expressionКомпилятор может вычислить их заранее и подставлять литералы.
Run-time constants получают значение из выражений, известных только в runtime:
int input;
cin >> input;
const int x = input + 5; // Value not known until runtimeОбъект по-прежнему неизменяем, но компилятор не может «свернуть» его в константу на этапе компиляции.
На практике: compile-time-константы допускаются в большем числе контекстов (например, размер массива в стандартном C++), run-time-константы дают неизменяемость без знания значения на этапе компиляции.
1.9.3 Constant expressions
Constant expressions — выражения, вычислимые компилятором при компиляции. Они требуются, например, для:
- размеров массива:
const int size = 10; int arr[size]; - параметров шаблонов
- меток
caseвswitch
1.9.4 const и указатели
С pointers и const возможны четыре базовые комбинации:
T* ptr;— обычный указатель на изменяемый объектconst T* ptr;— указатель на const-объект (через указатель менять нельзя)T* const ptr = &obj;— const pointer (адрес в указателе не меняется)const T* const ptr = &obj;— константный указатель на константный объект
int x = 5;
const int y = 10;
int* p1 = &x; // Can modify x through p1
const int* p2 = &y; // Cannot modify through p2
int* const p3 = &x; // p3 always points to x
const int* const p4 = &y; // p4 always points to y, cannot modify
*p1 = 7; // OK
*p2 = 7; // ERROR
p3 = &y; // ERRORКак читать объявление: справа налево.
const int* ptr— «ptr— указатель наint, который const»int* const ptr— «ptr— const-указатель наint»
1.9 Вывод типов с auto
1.9.1 Спецификатор auto
Ключевое слово auto (в современном C++ оно не storage-class specifier) велит компилятору вывести тип переменной из инициализатора — это type deduction / type inference.
auto x = 7; // x has type int
auto y = 3.14; // y has type double
auto z = "hello"; // z has type const char*
auto v = new vector<int>; // v has type vector<int>*Зачем: имена типов в C++ часто громоздкие; auto смещает акцент на логику.
1.9.2 Правила вывода для auto
Для auto var = expression;:
| Тип выражения | Выведенный тип var |
|---|---|
T* или const T* |
T* или const T* (указатели сохраняются) |
T, const T, T&, const T& |
T (const и ссылка сбрасываются) |
Для auto& var = expression;:
| Тип выражения | Выведенный тип var |
|---|---|
T |
ошибка |
const T |
const T& |
T& |
T& |
int x = 5;
const int y = 10;
int& rx = x;
auto a = x; // a has type int (not int&)
auto b = y; // b has type int (const dropped)
auto c = rx; // c has type int (reference dropped)
auto& d = x; // d has type int&
auto& e = y; // e has type const int&Важно: auto не значит «любой тип», а «вывести точный тип из инициализатора».
1.9.3 Плюсы auto
Короче запись — особенно для сложных типов:
// Instead of:
vector<double*>* v = new vector<double*>(77);
// Write:
auto v = new vector<double*>(77);Поддерживаемость: если поменялся тип, возвращаемый функцией, фрагменты с auto часто остаются корректными.
Согласованность типов: меньше неявных «лишних» преобразований.
Осторожно: злоупотребление auto ухудшает читаемость; используйте, когда тип очевиден из контекста или слишком длинен.
1.9.4 Ограничения
- нужен инициализатор:
auto x;— ошибка; - в одном объявлении нельзя смешивать несовместимые выводы:
auto a = 5, b = {1, 2};— ошибка; autoбольше не класс памяти:auto int x;— ошибка.
1.10 Структурированная привязка (structured binding, C++17)
1.10.1 Базовый синтаксис
Structured binding (структурированная привязка, C++17) разбивает объект на части и вводит имена для этих частей одной декларацией.
Синтаксис:
auto [var1, var2, var3] = expression;
auto [var1, var2, var3] { expression };
auto [var1, var2, var3] ( expression );В текущей области появляются var1, var2, var3, привязанные к подобъектам или элементам результата expression.
Наглядно: как распаковать чемодан — один составной объект разделяется на именованные элементы.
1.10.2 Structured binding и массивы
Для массива:
int a[2] = { 1, 2 };
auto [x, y] = a; // x and y are copies of a[0] and a[1]
auto& [xr, yr] = a; // xr and yr are references to a[0] and a[1]По значению (auto [x, y] = a;):
- создаётся временная копия массива;
xиyотносятся к элементам копии;- изменения
x/yне трогают исходныйa.
По ссылке (auto& [x, y] = a;):
- полной копии нет;
xиy— прямые ссылки на элементыa;- изменения видны в
a.
1.10.3 Structured binding и структуры
Для struct привязка идёт по полям:
struct S {
int x;
const double y;
};
S s = {1, 3.14};
auto [a, b] = s; // a is int, b is const double
const auto [c, d] = s; // c is const int, d is const double1.10.4 Tuple и pair
Работает с std::tuple и std::pair:
std::tuple<int, int&> f() { ... }
auto [x, y] = f(); // x is int, y is int&
const auto [z, w] = f(); // z is const int, w is int& (reference not affected by const)2. Определения
- Pointer (указатель): переменная, хранящая memory address другой переменной; объявляется с
*(например,int* ptr). - Address-of operator (
&) (оператор взятия адреса): возвращает адрес переменной (&x— адресx). - Dereference operator (
*) (оператор разыменования): доступ к значению по адресу, записанному в указателе (*ptr). - Null pointer (
nullptr): специальное значение указателя «ни на что валидное не ссылается». - Pointer arithmetic (арифметика указателей): операции над указателями (например,
ptr++— к следующему элементу). - Namespace (пространство имён): декларативная область для идентификаторов, снижающая коллизии имён.
- Qualified name (квалифицированное имя): имя с указанием пространства имён:
namespace::identifier. - Scope resolution operator (
::) (оператор разрешения области видимости): доступ к членам пространства имён или класса. - Using declaration (using-декларация): подключает член пространства имён в текущую область (например,
using std::cout;). - Array (массив): непрерывный набор элементов одного типа фиксированного размера; размер известен на этапе компиляции.
- Array decay (преобразование массива в указатель): автоматическое превращение имени массива в указатель на первый элемент.
- Vector (вектор): динамический массив из C++ Standard Library; заголовок
<vector>. - Range-based for loop: цикл C++11
for (element : container)по элементам контейнера или массива. - Reference (ссылка): псевдоним существующего объекта; после инициализации не меняет объект-референт; сама ссылка не является объектом в обычном смысле.
- Constant type (константный тип): тип с квалификатором
const; объекты нельзя менять после инициализации. - Compile-time constant (константа времени компиляции): значение задаётся constant expression на этапе компиляции.
- Run-time constant (константа времени выполнения): значение известно в runtime, но дальше объект неизменяем.
- Constant expression (константное выражение): выражение, вычислимое компилятором при компиляции.
- Type deduction (вывод типа): способность компилятора определить тип переменной по инициализатору.
autospecifier: спецификатор типа «вывести тип автоматически».- Structured binding (структурированная привязка, C++17): одна декларация вводит несколько имён для частей объекта.
- Preprocessor directive (директива препроцессора): команда с
#, обрабатываемая до компиляции (#includeи т.д.). - Stream insertion operator (
<<) (оператор вставки в поток): перегрузка для вывода в потоки вродеstd::cout. - Heap (куча): область памяти для динамически выделенных объектов; управление на программисте.
- Stack (стек): область для локальных переменных и кадров вызовов; управляется по правилам области видимости.
- Type (тип): классификация: допустимые значения, операции и отношения с другими типами.
- lvalue: выражение, обозначающее место в памяти и допустимое в левой части присваивания.
- Indirection (косвенный доступ): доступ к значению через pointer или reference, а не напрямую.
3. Примеры
3.1. Конвертер длительности (секунды → ч:м:с) (Лаба 1, Задание 1)
Напишите программу: на вход — длительность в секундах, на выход — в формате часы : минуты : секунды.
Пример:
| Ввод | Вывод |
|---|---|
| 124660 | 34:37:40 |
Нажмите, чтобы увидеть решение
Ключевая идея: целочисленное деление для часов и минут; оператор % для остатков.
#include <iostream>
using namespace std;
int main()
{
int totalSeconds;
cin >> totalSeconds;
// Calculate hours, minutes, and seconds
int hours = totalSeconds / 3600;
int minutes = (totalSeconds % 3600) / 60;
int seconds = totalSeconds % 60;
// Print in required format
cout << hours << ":" << minutes << ":" << seconds << endl;
return 0;
}Разбор:
- Часы:
totalSeconds / 3600(целая часть — часы).- \(124660 \div 3600 = 34\) часа
- Остаток после часов:
% 3600.- \(124660 \mod 3600 = 2260\) секунд
- Минуты: остаток делим на 60.
- \(2260 \div 60 = 37\) минут
- Секунды: остаток от деления на 60.
- \(2260 \mod 60 = 40\) секунд
Ответ: для ввода 124660 вывод 34:37:40
Замечание: ведущие нули (34:07:05) потребуют std::setw и std::setfill из <iomanip>.
3.2. Обмен значений через указатели (Лаба 1, Задание 2a)
Реализуйте свою функцию обмена двух целых, передавая аргументы by pointer (через pointers).
Нажмите, чтобы увидеть решение
Ключевая идея: в pointer хранится address; чтобы поменять исходные переменные, нужно dereference и работать со значениями по этим адресам.
#include <iostream>
using namespace std;
// Swap function using pointers
void swapByPointer(int* a, int* b)
{
int temp = *a; // Store value pointed to by a
*a = *b; // Set value at a to value at b
*b = temp; // Set value at b to saved temp value
}
int main()
{
int x = 5, y = 10;
cout << "Before swap: x = " << x << ", y = " << y << endl;
swapByPointer(&x, &y); // Pass addresses of x and y
cout << "After swap: x = " << x << ", y = " << y << endl;
return 0;
}Разбор:
- Сигнатура:
void swapByPointer(int* a, int* b)— параметры — pointers наint. - Dereference: оператор
*даёт значение по адресу;*a = *bкопирует значение из «ячейки»bв «ячейку»a. - Вызов: передаём адреса:
swapByPointer(&x, &y).
Ответ: значения исходных переменных меняются местами.
3.3. Обмен значений через ссылки (Лаба 1, Задание 2b)
Та же задача, но передача by reference (references).
Нажмите, чтобы увидеть решение
Ключевая идея: reference — alias; изменяя ссылку, вы меняете тот же объект в памяти.
#include <iostream>
using namespace std;
// Swap function using references
void swapByReference(int& a, int& b)
{
int temp = a; // a is a reference, directly accesses the original
a = b; // Assign value of b to a
b = temp; // Assign saved value to b
}
int main()
{
int x = 5, y = 10;
cout << "Before swap: x = " << x << ", y = " << y << endl;
swapByReference(x, y); // Pass variables directly (not addresses)
cout << "After swap: x = " << x << ", y = " << y << endl;
return 0;
}Разбор:
void swapByReference(int& a, int& b)— параметры — references наint.- Разыменование не требуется:
aиb— это самиxиy(не «ячейки по адресу», а прямые псевдонимы). - Вызов:
swapByReference(x, y).
Сравнение с указателями:
- короче запись в теле функции;
- нет null references;
- намерение «работаем с оригиналом» читается проще.
Ответ: результат тот же, что и у варианта с pointers.
3.4. Удаление дубликатов (массив) (Лаба 1, Задание 3a)
Ввод: \(N\), затем \(N\) целых. Удалите все дубликаты, используя только arrays (массивы).
Пример:
| Ввод | Вывод |
|---|---|
| 8 1 3 5 3 3 4 1 2 |
1 3 5 4 2 |
Нажмите, чтобы увидеть решение
Ключевая идея: вести массив уже встреченных уникальных значений и для каждого нового элемента проверять, не был ли он добавлен ранее.
#include <iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int arr[n]; // Note: Variable-length arrays are not standard C++
// but supported by some compilers
// Read input
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
// Array to store unique elements
int unique[n];
int uniqueCount = 0;
// For each element in original array
for (int i = 0; i < n; i++) {
bool isDuplicate = false;
// Check if element already exists in unique array
for (int j = 0; j < uniqueCount; j++) {
if (arr[i] == unique[j]) {
isDuplicate = true;
break;
}
}
// If not a duplicate, add to unique array
if (!isDuplicate) {
unique[uniqueCount] = arr[i];
uniqueCount++;
}
}
// Print unique elements
for (int i = 0; i < uniqueCount; i++) {
cout << unique[i];
if (i < uniqueCount - 1) cout << " ";
}
cout << endl;
return 0;
}Разбор:
- читаем все \(N\) значений в массив;
- второй массив — только уникальные;
- для каждого элемента исходного массива проверяем наличие во «втором»; если нет — добавляем и увеличиваем
uniqueCount; - печатаем уникальные подряд.
Время: \(O(n^2)\) из‑за вложенных циклов. Память: \(O(n)\).
Ответ: для примера из условия — 1 3 5 4 2.
Замечание: int arr[n] (VLA) не входит в стандарт C++, хотя некоторые компиляторы это принимают; в «каноническом» C++ — динамика или vectors (следующий пункт).
3.5. Удаление дубликатов (vector) (Лаба 1, Задание 3b)
Те же входные данные, но решение на std::vector.
Пример:
| Ввод | Вывод |
|---|---|
| 8 1 3 5 3 3 4 1 2 |
1 3 5 4 2 |
Нажмите, чтобы увидеть решение
Ключевая идея: vectors дают динамический размер и удобные методы вроде push_back(); алгоритм тот же, синтаксис проще.
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> arr(n); // Vector of size n
// Read input
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
vector<int> unique; // Vector to store unique elements
// For each element in original vector
for (int i = 0; i < n; i++) {
bool isDuplicate = false;
// Check if element already exists in unique vector
for (int j = 0; j < unique.size(); j++) {
if (arr[i] == unique[j]) {
isDuplicate = true;
break;
}
}
// If not a duplicate, add to unique vector
if (!isDuplicate) {
unique.push_back(arr[i]);
}
}
// Print unique elements
for (int i = 0; i < unique.size(); i++) {
cout << unique[i];
if (i < unique.size() - 1) cout << " ";
}
cout << endl;
return 0;
}Разбор:
vector<int> arr(n)— сразу \(n\) элементов (можно читать и в пустойvectorчерезpush_back).vector<int> uniqueрастёт по мере добавления.push_backдобавляет в конец.size()возвращает текущую длину (у сырого массива длины «в типе» нет).
Плюсы перед массивом: не вести размер вручную, проще границы (.at()), нагляднее код.
Вариант с range-based for:
// Checking for duplicates using range-based for
for (int elem : arr) {
bool isDuplicate = false;
for (int uElem : unique) {
if (elem == uElem) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
unique.push_back(elem);
}
}Ответ: вывод как в 3.4.
Быстрее: сортировка и сжатие подряд идущих дубликатов даёт порядок \(O(n \log n)\); std::set хранит уникальность автоматически.
3.6. Вывод типов для auto (Лекция 1, Пример 1)
Определите типы переменных в объявлениях:
auto x = 7;auto a[] = { 1, 2, 3 };const auto *v = &x;static auto y = 0.0;auto int r;auto m;auto a=5, b={1,2};
Нажмите, чтобы увидеть решение
Ключевая идея: правила type deduction для auto.
Ответы:
(a) auto x = 7; — x имеет тип int (литерал 7).
(b) auto a[] = { 1, 2, 3 }; — тип a: std::initializer_list<int> (фигурный список в такой форме).
(c) const auto *v = &x; (при int x из (a)) — v: const int* (pointer to const int).
(d) static auto y = 0.0; — y: double; static на вывод типа не влияет.
(e) auto int r; — ошибка: в современном C++ auto не комбинируется с явным int.
(f) auto m; — ошибка: у auto должен быть инициализатор.
(g) auto a=5, b={1,2}; — ошибка: в одной декларации несовместимые выведенные типы (int и initializer_list<int>).
Итог: корректны (a)–(d); (e)–(g) не компилируются.
3.7. Примеры со ссылками (Лекция 1, Пример 2)
Разберите код со references:
void f ( double& a )
{ a += 3.14; }
double d = 7.0;
f(d);Каково значение d после вызова f(d)?
Также разберите:
int v[20];
int& f ( int i ) { return v[i]; }
f(3) = 7;Что делает этот фрагмент?
Нажмите, чтобы увидеть решение
Ключевая идея: по reference функция может менять аргумент; возвращаемая ссылка может быть lvalue.
Часть 1: изменение аргумента
void f ( double& a )
{ a += 3.14; }
double d = 7.0;
f(d); // After this call, d = 10.14Разбор: a — reference на d; a += 3.14 меняет сам d. Ответ: d == 10.14.
Сравнение с передачей по значению:
void f ( double a ) // No reference
{ a += 3.14; }
double d = 7.0;
f(d); // d still equals 7.0 (unchanged)Часть 2: ссылка как lvalue
int v[20];
int& f ( int i ) { return v[i]; }
f(3) = 7; // This assigns 7 to v[3]Разбор: f возвращает reference на v[i]; выражение f(3) = 7 эквивалентно v[3] = 7. Ответ: v[3] == 7.
Так делают, например, перегрузки operator[], чтобы можно было писать matrix[i][j] = 5;.
3.8. Указатели и const (Лекция 1, Пример 3)
По объявлениям ниже — какие операции допустимы?
int x = 5;
const int y = 10;
int* p1 = &x;
const int* p2 = &y;
int* const p3 = &x;
const int* const p4 = &y;
*p1 = 7; // Operation A
*p2 = 7; // Operation B
p3 = &y; // Operation CНажмите, чтобы увидеть решение
Ключевая идея: четыре сочетания pointer и const.
Типы:
int* p1— обычный указатель на изменяемыйint.const int* p2— pointer to const (через указатель не пишем; сам указатель переназначать можно).int* const p3— const pointer (адрес фиксирован; объект по адресу менять можно).const int* const p4— константный указатель на константу.
Чтение справа налево: int* const p, const int* p.
Операции:
- A
*p1 = 7— корректно;xстанет7. - B
*p2 = 7— ошибка (черезconst int*писать нельзя). - C
p3 = &y— ошибка (const pointer не переназначаем) плюс несовместимостьint*иconst int*.
Допустимо, например:
p2 = &x; // VALID: can reassign p2
*p3 = 100; // VALID: can modify through p3
int z = *p2; // VALID: can read through p2
int w = *p4; // VALID: can read through p4| Тип | Менять объект? | Менять указатель? |
|---|---|---|
int* |
да | да |
const int* |
нет | да |
int* const |
да | нет |
const int* const |
нет | нет |
Ответ: из трёх показанных в условии допустима только A.
3.9. Поиск в массиве (версия 1) (Туториал 1, Пример 1)
Найти значение в массиве фиксированного размера.
int find1 ( int array[20], int x )
{
for ( int i = 0; i < 20; i++ )
{
if ( array[i] == x ) return i; // success
}
return -1; // fail
}Нажмите, чтобы увидеть решение
Ключевая идея: линейный проход; индекс при успехе, -1 при неудаче.
Замечания: размер «зашит» в 20 — негибко; константа дублируется (DRY). Сложность \(O(n)\) при \(n=20\).
Ответ: работает, но лучше обобщить (следующий пример).
3.10. Поиск в массиве (версия 2) (Туториал 1, Пример 2)
То же через pointers и произвольный размер \(n\).
int* find2 ( int* array, int n, int x )
{
const int* p = array;
for ( int i = 0; i < n; i++ )
{
if ( *p == x ) return p; // success
p++;
}
return nullptr; // fail
}Использование:
int A[20];
// ... fill array ...
int* res = find2(A, 20, 5);Нажмите, чтобы увидеть решение
Ключевая идея: массив передаётся как указатель на первый элемент; возвращаем указатель на найденное или nullptr.
Плюсы к версии 1: параметр \(n\); pointer arithmetic (p++); наглядная семантика «не найдено».
Ответ: заметно универсальнее версии 1.
3.11. Изменение элементов vector (Туториал 1, Пример 3)
Чему равны элементы v6 после цикла?
vector<int> v6 = { 1, 2, 3, 4 };
for ( int elem : v6 )
elem = elem * 10;Нажмите, чтобы увидеть решение
Ключевая идея: в range-based for важно, копия это или reference.
Ответ: { 1, 2, 3, 4 } — не изменились: elem — копия.
Правильно для изменения:
vector<int> v6 = { 1, 2, 3, 4 };
for ( int& elem : v6 ) // Note the & (reference)
elem = elem * 10;
// Now v6 = { 10, 20, 30, 40 }С auto:
for ( auto& elem : v6 ) // Compiler deduces type
elem = elem * 10;Правило: только чтение — по значению; правка — Type& или auto&; крупные объекты без копии — const Type&.
3.12. Structured binding и массив (Туториал 1, Пример 4)
Что означают привязки и как связаны переменные?
int a[2] = { 1, 2 };
auto [x, y] = a;
auto& [xr, yr] = a;Нажмите, чтобы увидеть решение
Ключевая идея: копия массива vs references в structured binding.
auto [x, y] = a — временная копия; x, y — элементы копии.
auto& [xr, yr] = a — xr, yr ссылаются на a[0], a[1].
x = 100; // Only changes x, not a[0]
xr = 200; // Changes both xr AND a[0]
cout << a[0]; // Prints: 200
cout << x; // Prints: 100Ответ: x,y — независимые копии; xr,yr — псевдонимы элементов a.
3.13. Structured binding и struct (Туториал 1, Пример 5)
Каковы типы переменных после привязки?
struct S {
int x;
const double y;
};
S f() { return S{5, 3.14}; }
const auto [x, y] = f();Нажмите, чтобы увидеть решение
Ключевая идея: const auto в structured binding добавляет верхнеуровневый const к привязкам; const полей сохраняется.
У S: int x, const double y. Для const auto [x, y] = f();:
x—const int(const отconst auto);y—const double(const поля сохраняется).
Без const auto:
auto [x, y] = f();
// x would be: int
// y would be: const double (const from member preserved)Правило: const члена в привязке не «снимается»; верхний const задаётся спецификатором привязки.